/* 
 * Copyright 2008 Lovely Systems GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lovely.gwt.jsonrpc.rebind;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import lovely.gwt.jsonrpc.client.Cancelable;
import lovely.gwt.jsonrpc.client.JSONServiceBase;
import lovely.gwt.jsonrpc.client.RemoteJSONService;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * Generator of JSON RPC wrappers.
 */
public class JSONProxyGenerator extends Generator {

  Set<String> alreadyProcessed;

  SourceWriter wrapperSourceWriter;
  StringWriter debugWriter;
  int debugIndent;
  private final String tabString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

  public JSONProxyGenerator() {
    alreadyProcessed = new HashSet<String>(0);
  }

  public String generate(TreeLogger logger, GeneratorContext context,
      String typeName) throws UnableToCompleteException {
    if (!alreadyProcessed.contains(typeName)) {
      alreadyProcessed.add(typeName);
    }
    TypeOracle oracle = context.getTypeOracle();
    JClassType wrappedInterface = oracle.findType(typeName);

    if (!wrappedInterface.isAssignableTo(oracle
        .findType(RemoteJSONService.class.getCanonicalName()))) {
      logAndThrow(logger, "Type " + typeName
          + " should inherit from RemoteJSONService.");
    }

    JClassType wrappedAsyncInterface = oracle.findType(typeName + "Async");
    checkInterfaces(logger, wrappedInterface, wrappedAsyncInterface);
    String wrapperClassName = simpleStubClassName(wrappedAsyncInterface
        .getName());
    wrapperSourceWriter = getSourceWriter(logger, context,
        wrappedAsyncInterface.getPackage().getName(), wrapperClassName,
        wrappedAsyncInterface.getName(), wrappedInterface);

    if (logger.isLoggable(TreeLogger.ALL)) {
      debugWriter = new StringWriter();
    } else {
      debugWriter = null;
    }
    debugWriter = new StringWriter();
    Set<Object> typesToDeserialize = new HashSet<Object>();
    Set<Object> typesToSerialize = new HashSet<Object>();
    if (wrapperSourceWriter != null) {
      indent();
      JMethod[] methods = wrappedAsyncInterface.getMethods();
      for (int i = 0; i < methods.length; i++) {
        try {
          JMethod meth = methods[i];
          Object ignore = meth.getAnnotation(Ignore.class);
          if (ignore != null) continue;
          generateMethodWrapper(context, typesToDeserialize, typesToSerialize,
              methods[i], wrappedInterface.getMethods()[i].getReturnType());
        } catch (NotFoundException e) {
          throw new UnableToCompleteException();
        }
      }

      Object[] toDeserialize = typesToDeserialize.toArray();
      for (int i = 0; i < toDeserialize.length; i++) {
        generateTypeDeserializer((JType) toDeserialize[i]);
      }

      Object[] toSerialize = typesToSerialize.toArray();
      for (int i = 0; i < toSerialize.length; i++) {
        generateTypeSerializer((JType) toSerialize[i]);
      }
      outdent();
      wrapperSourceWriter.commit(logger);
      // System.out.println(debugWriter.toString());
      if (logger.isLoggable(TreeLogger.ALL)) {
        logger.log(TreeLogger.ALL, debugWriter.toString(), null);
      }
    }
    return wrappedAsyncInterface.getPackage().getName() + "."
        + wrapperClassName;
  }

  private void checkInterfaces(TreeLogger logger, JClassType wrappedInterface,
      JClassType wrappedAsyncInterface) throws UnableToCompleteException {
    if (wrappedAsyncInterface == null) {
      logAndThrow(logger, "Can't find async interface for "
          + wrappedInterface.getQualifiedSourceName());
    }
    JMethod[] methods = wrappedInterface.getMethods();
    JMethod[] asyncMethods = wrappedAsyncInterface.getMethods();
    if (methods.length != asyncMethods.length) {
      logAndThrow(logger, "Interfaces "
          + wrappedInterface.getQualifiedSourceName() + " and "
          + wrappedAsyncInterface.getQualifiedSourceName()
          + " should have the same number of methods.");
    }
    Map<String, JMethod> fullMethodsMap = createMethodsMap(logger, methods,
        false);
    Map<String, JMethod> fullAsyncMethodsMap = createMethodsMap(logger,
        asyncMethods, true);

    Object[] fullMethodNames = fullMethodsMap.keySet().toArray();
    for (int i = 0; i < fullMethodNames.length; i++) {
      String methodName = (String) fullMethodNames[i];
      if (!fullAsyncMethodsMap.containsKey(methodName)) {
        logAndThrow(logger, "Couldn't find async method for "
            + getReadableMethodName(((JMethod) fullMethodsMap.get(methodName))));
      }
    }
  }

  private Map<String, JMethod> createMethodsMap(TreeLogger logger,
      JMethod[] methods, boolean fromAsyncInterface)
      throws UnableToCompleteException {
    Map<String, JMethod> methodsMap = new HashMap<String, JMethod>();
    for (int i = 0; i < methods.length; i++) {
      JMethod method = methods[i];
      if (fromAsyncInterface
          && method.getReturnType() != JPrimitiveType.VOID
          && !method.getReturnType().getQualifiedSourceName().equals(
              Cancelable.class.getCanonicalName())) {
        logAndThrow(logger, "Method: " + getReadableMethodName(method)
            + " should return void or Cancelable.");
      }
      StringBuilder methodName = new StringBuilder(method.getName());
      JParameter[] params = method.getParameters();
      int paramsCount = params.length - (fromAsyncInterface ? 1 : 0);
      for (int j = 0; j < paramsCount; j++) {
        methodName.append(getSafeTypeName(params[j].getType()));
      }
      if (fromAsyncInterface
          && (params.length == 0 || !params[params.length - 1].getType()
              .getQualifiedSourceName().equals(
                  AsyncCallback.class.getCanonicalName()))) {
        logAndThrow(logger, "Method's " + getReadableMethodName(method)
            + " last param should have be of type AsyncCallback");
      }
      methodsMap.put(methodName.toString(), method);
    }
    return methodsMap;
  }

  private static String getReadableMethodName(JMethod method) {
    return String.format("%1$s:%2$s", new Object[] {
        method.getEnclosingType().getQualifiedSourceName(),
        method.getReadableDeclaration(true, true, true, true, true) });
  }

  private static void logAndThrow(TreeLogger logger, String errorMessage)
      throws UnableToCompleteException {
    logger.log(TreeLogger.ERROR, errorMessage, null);
    throw new UnableToCompleteException();
  }

  private void generateTypeSerializer(JType type) {
    String shortTypeName = type.getSimpleSourceName();
    println(String.format(
        "private JSONValue serialize_%1$s(%2$s __toserialize__) {",
        new Object[] { getSafeTypeName(type), shortTypeName }));
    indent();
    println("if (__toserialize__ == null) {");
    indent();
    println("return JSONNull.getInstance();");
    outdent();
    println("};");
    if (type.isArray() != null) {
      println("JSONArray __result__ = new JSONArray();");
      println("for (int __i__ = 0; __i__ < __toserialize__.length; __i__++) {");
      indent();
      println(String
          .format(
              "__result__.set(__i__, serialize_%1$s(__toserialize__[__i__]));",
              new Object[] { getSafeTypeName(((JArrayType) type)
                  .getComponentType()) }));
      outdent();
      println("};");
    } else {
      println("JSONObject __result__ = new JSONObject();");
      JMethod[] methods = ((JClassType) type).getMethods();
      for (int i = 0; i < methods.length; i++) {
        JMethod method = methods[i];
        Object ignore = method.getAnnotation(Ignore.class);
        if (ignore != null) continue;
        if (method.getParameters().length == 0) {
          String propertyName = methodNameToPropName(method.getName());
          if (propertyName != null) {
            String typeName = getSafeTypeName(method.getReturnType());
            String line = String
                .format(
                    "__result__.put(\"%1$s\", serialize_%2$s(__toserialize__.%3$s()));",
                    new Object[] { propertyName, typeName, method.getName() });
            println(line);
          }

        }
      }
    }
    println("return __result__;");
    outdent();
    println("}");
  }

  public static String methodNameToPropName(String methodName) {
    String propName = methodName;
    if (methodName.startsWith("get")) {
      propName = methodName.substring(3);
    } else if (methodName.startsWith("is")) {
      propName = methodName.substring(2);
    } else {
      return null;
    }
    propName = propName.substring(0, 1).toLowerCase() + propName.substring(1);
    return propName;
  }

  private void generateTypeDeserializer(JType type) {
    String shortTypeName = type.getSimpleSourceName();
    // Hack for Lists
    // if (shortTypeName.equals("List")) {
    // return;
    // }
    println(String.format(
        "private %1$s deserialize_%2$s(JSONValue __deserialized__) {",
        new Object[] { shortTypeName, getSafeTypeName(type) }));
    indent();
    println("if (__deserialized__.isNull() != null) {");
    indent();
    println("return null;");
    outdent();
    println("};");
    if (type.isArray() != null) {
      String arraySuffix = "";
      while (shortTypeName.endsWith("[]")) {
        arraySuffix += "[]";
        shortTypeName = shortTypeName.substring(0, shortTypeName.length() - 2);
      }
      JType componentType = ((JArrayType) type).getComponentType();
      println("JSONArray __array__ = (JSONArray)__deserialized__;");
      println(String.format(
          "%1$s[]%2$s __result__ = new %1$s[__array__.size()]%2$s;",
          new Object[] { shortTypeName,
              arraySuffix.substring(0, arraySuffix.length() - 2) }));
      println("for (int __i__ = 0; __i__ < __array__.size(); __i__++) {");
      indent();
      println(String.format(
          "__result__[__i__] = deserialize_%1$s(__array__.get(__i__));",
          new Object[] { getSafeTypeName(componentType) }));
      outdent();
      println("};");
    } else {
      println("JSONObject __object__ = (JSONObject)__deserialized__;");
      println(String.format("%1$s __result__ = new %1$s();",
          new Object[] { shortTypeName }));
      println("JSONValue __value__ = null;");
      JMethod[] methods = ((JClassType) type).getMethods();
      for (int i = 0; i < methods.length; i++) {
        JMethod method = methods[i];
        Object ignore = method.getAnnotation(Ignore.class);
        if (ignore != null) continue;
        if (method.getParameters().length == 1) {
          if (method.getName().startsWith("set")) {
            String propertyName = method.getName().substring(3);
            println("__value__ = __object__.get(\"" + propertyName + "\");");
            println("if (__value__ == null) {");
            indent();
            println("__value__ = __object__.get(\""
                + propertyName.substring(0, 1).toLowerCase()
                + propertyName.substring(1) + "\");");
            outdent();
            println("};");
            println("if (__value__ != null) {");
            indent();
            println(String
                .format("__result__.%1$s(deserialize_%2$s(__value__));",
                    new Object[] {
                        method.getName(),
                        getSafeTypeName(method.getParameters()[0].getType()),
                        method.getParameters()[0].getType()
                            .getSimpleSourceName() }));
            outdent();
            println("};");
          }
        }
      }
    }
    println("return __result__;");
    outdent();
    println("}");
  }

  private void generateMethodWrapper(GeneratorContext context,
      Set<Object> typesToDeserialize, Set<Object> typesToSerialize,
      JMethod asyncMethod, JType returnType) throws NotFoundException {
    boolean returnsCancelable = asyncMethod.getReturnType() != JPrimitiveType.VOID;
    print("public " + (returnsCancelable ? "Cancelable " : "void "));
    print(asyncMethod.getName() + "(");
    JParameter[] originalParams = asyncMethod.getParameters();
    NameTypePair[] params = new NameTypePair[originalParams.length - 1];
    String asyncCallbackName = originalParams[originalParams.length - 1]
        .getName();
    for (int i = 0; i < originalParams.length; i++) {
      if (i > 0) {
        print(", ");
      }
      print(originalParams[i].getType().getSimpleSourceName());
      print(" ");
      print(originalParams[i].getName());
      if (i < originalParams.length - 1) {
        params[i] = new NameTypePair(originalParams[i].getName(),
            originalParams[i].getType());
      }
    }
    println(") {");
    indent();
    println("JSONObject __request__ = new JSONObject();");
    // version
    println("__request__.put(\"version\", new JSONString(\"1.1\"));");
    // id (currently not handled in response)
    println("__request__.put(\"id\", new JSONString(\"0\"));");
    // method
    println("__request__.put(\"method\", new JSONString(\""
        + asyncMethod.getName() + "\"));");
    // parameters (optional)
    if (params.length > 0) {
      println("JSONObject __params__ = new JSONObject();");
      for (int i = 0; i < params.length; i++) {
        writeParam(typesToSerialize, "__params__", "", "", params[i]);
      }
      println("__request__.put(\"params\", __params__);");
    } else {
      println("__request__.put(\"params\", new JSONObject());");    	
    }

    println("System.out.println(\"JSONProxyGenerator.beforeSend: \" + __request__);");
    println(String.format("if (%1$s instanceof DebugAsyncCallback) {",
        new Object[] { asyncCallbackName }));
    indent();
    println(String.format(
        "((DebugAsyncCallback)%1$s).beforeSend(__request__);",
        new Object[] { asyncCallbackName }));
    outdent();
    println("}");
    println("RequestBuilder __builder__ = createBuilder(\""
        + asyncMethod.getName() + "\");");
    createDeserializingCallback(context, typesToDeserialize, returnType,
        asyncCallbackName);

    print("__callback__.setInfo(\"url:\\t\" + __builder__.getUrl() + \"\\ndata:\\t\" + __request__);");
    print("__callback__.setRequestData(__builder__, __request__.toString());");
    
    println("try {");
    indent();
    if (returnsCancelable) {
      print("return new CancelableRequest(");
    }
    print("__builder__.sendRequest(__request__.toString(), __callback__)");
    if (returnsCancelable) {
      println(");");
    } else {
      println(";");
    }
    outdent();
    println("} catch(RequestException __exception__) {");
    indent();
    println("callback.onFailure(__exception__);");
    outdent();
    println("};");
    outdent();
    if (returnsCancelable) {
      println("return null;");
    }
    println("}");

  }

  private void createDeserializingCallback(GeneratorContext context,
      Set<Object> typesToDeserialize, JType returnType, String asyncCallbackName)
      throws NotFoundException {
    String returnTypeName = returnType.getSimpleSourceName();

    println("JSONRequestCallback __callback__ = new JSONRequestCallback("
        + asyncCallbackName + ") {");
    indent();

    // handle Strings
    println("public Object deserialize(String __response__) {");
    indent();
    if (returnTypeName == "void") {
      println("return null;");
    } else {
      println("JSONValue __jsonValue__ = JSONParser.parse(__response__);");
      println("return deserialize(__jsonValue__);");
    }
    outdent();
    println("}");

    // handle JSONValues
    println("public Object deserialize(JSONValue __jsonValue__) {");
    indent();
    if (returnTypeName == "void") {
      println("return null;");
    } else {
      String safeTypeName;
      if (returnType.isPrimitive() != null) {
        safeTypeName = getSafeTypeName(mapPrimitiveToObject(context, returnType));
      } else {
        safeTypeName = getSafeTypeName(returnType);
      }
      println("return deserialize_" + safeTypeName + "(__jsonValue__);");

      if (typesToDeserialize != null) {
        traverseAllSubTypes(typesToDeserialize, returnType);
      }
    }
    outdent();
    println("}");
    outdent();
    println("};");
  }

  private JType mapPrimitiveToObject(GeneratorContext context, JType type)
      throws NotFoundException {
    if (type == JPrimitiveType.BOOLEAN) {
      return context.getTypeOracle().getType("java.lang.Boolean");
    }
    if (type == JPrimitiveType.BYTE) {
      return context.getTypeOracle().getType("java.lang.Byte");
    }
    if (type == JPrimitiveType.SHORT) {
      return context.getTypeOracle().getType("java.lang.Short");
    }
    if (type == JPrimitiveType.INT) {
      return context.getTypeOracle().getType("java.lang.Integer");
    }
    if (type == JPrimitiveType.LONG) {
      return context.getTypeOracle().getType("java.lang.Long");
    }
    if (type == JPrimitiveType.FLOAT) {
      return context.getTypeOracle().getType("java.lang.Float");
    }
    if (type == JPrimitiveType.DOUBLE) {
      return context.getTypeOracle().getType("java.lang.Double");
    }
    if (type == JPrimitiveType.CHAR) {
      return context.getTypeOracle().getType("java.lang.Character");
    }
    return (JType) null;
  }

  private boolean isPrimitiveOrStringType(JType type) {
    // TODO: mapping
    return type instanceof JPrimitiveType
        || type.getQualifiedSourceName().equals("java.lang.Boolean")
        || type.getQualifiedSourceName().equals("java.lang.Byte")
        || type.getQualifiedSourceName().equals("java.lang.Short")
        || type.getQualifiedSourceName().equals("java.lang.Integer")
        || type.getQualifiedSourceName().equals("java.lang.Long")
        || type.getQualifiedSourceName().equals("java.lang.Float")
        || type.getQualifiedSourceName().equals("java.lang.Double")
        || type.getQualifiedSourceName().equals("java.lang.Character")
        || type.getQualifiedSourceName().equals("java.lang.String");
  }

  private String getSafeTypeName(JType type) {
    type.getParameterizedQualifiedSourceName();
    String name = type.getParameterizedQualifiedSourceName().replaceAll("[.<>, ]", "_");
    // arrays can be part of the parameters
    return name.replace("[]", "_array_");
  }

  private void writeParam(Set<Object> typesToSerialize, String JSONObjectName,
      String paramPrefix, String paramSufix, NameTypePair param) {

    String line = String.format(
        "%1$s.put(\"%2$s\", serialize_%3$s(%4$s%2$s%5$s));", new Object[] {
            JSONObjectName, param.Name, getSafeTypeName(param.Type),
            paramPrefix, paramSufix });
    println(line);

    if (typesToSerialize != null) {
      traverseAllSubTypes(typesToSerialize, param.Type);
    }
  }

  private boolean traverseAllSubTypes(Set<Object> types, JType type) {

    if (isPrimitiveOrStringType(type)) {
      System.out.println("JSONProxyGenerator.traverseAllSubTypes: primitive "
          + type);
      return false;
    } else {
      System.out.println("JSONProxyGenerator.traverseAllSubTypes: non-primitive " + type );
      for (Iterator<Object> i = types.iterator(); i.hasNext();) {
        Object o = i.next();
        if (((JType)o).getParameterizedQualifiedSourceName() == type.getParameterizedQualifiedSourceName()) {
          return false;
        }
      }
//      if (types.contains(type))
//        return false;
      types.add(type);

      if (type.isArray() != null) {
        traverseAllSubTypes(types, ((JArrayType) type).getComponentType());
      }

      if (type.isClass() != null) {
        JMethod[] methods = ((JClassType) type).getMethods();
        for (int i = 0; i < methods.length; i++) {
          JMethod method = methods[i];
          Object ignore = method.getAnnotation(Ignore.class);
          if (ignore != null) continue;
          traverseAllSubTypes(types, method.getReturnType());
          JParameter[] params = method.getParameters();
          for (int j = 0; j < params.length; j++) {
            JParameter param = params[j];
            traverseAllSubTypes(types, param.getType());
          }
        }
      }
      return true;
    }
  }

  private Set<String> getAllTypes(JType type) {
    // Make sure the type isn't an array
    type = type.getLeafType();

    HashSet<String> m = new HashSet<String>();
    if(  type == JPrimitiveType.VOID
      || type.isWildcard() != null) {
      return m;
    }
    m.add(type.getQualifiedSourceName());
    JParameterizedType parameterizedType = type.isParameterized();
    if(parameterizedType != null) {
      JClassType[] params = parameterizedType.getTypeArgs();
      for(JClassType param : params) {
        m.addAll(getAllTypes(param));
      }
    }
    return m;
}

  private SourceWriter getSourceWriter(TreeLogger logger,
      GeneratorContext context, String packageName, String className,
      String interfaceName, JClassType wrappedInterface) {
    PrintWriter printWriter = context.tryCreate(logger, packageName, className);
    if (printWriter == null) {
      return null;
    }
    ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
        packageName, className);

    JMethod[] methods = wrappedInterface.getMethods();
    for (int i = 0; i < methods.length; i++) {
      JMethod method = methods[i];
      Object ignore = method.getAnnotation(Ignore.class);
      if (ignore != null) continue;

      JType returnType = methods[i].getReturnType();
      Set<String> classes = getAllTypes(returnType);
      for(String s : classes) {
        composerFactory.addImport(s);
      }
    }
    composerFactory.addImport("lovely.gwt.jsonrpc.client.Cancelable");
    composerFactory.addImport("lovely.gwt.jsonrpc.client.CancelableRequest");
    composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONServiceBase");
    composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONRequestCallback");
    composerFactory.addImport("lovely.gwt.jsonrpc.client.DebugAsyncCallback");
    composerFactory.addImport("com.google.gwt.user.client.rpc.AsyncCallback");
    composerFactory
        .addImport("com.google.gwt.user.client.rpc.ServiceDefTarget");
    composerFactory.addImport("com.google.gwt.http.client.RequestBuilder");
    composerFactory.addImport("com.google.gwt.http.client.RequestException");
    composerFactory.addImport("com.google.gwt.json.client.JSONParser");
    composerFactory.addImport("com.google.gwt.json.client.JSONValue");
    composerFactory.addImport("com.google.gwt.json.client.JSONArray");
    composerFactory.addImport("com.google.gwt.json.client.JSONBoolean");
    composerFactory.addImport("com.google.gwt.json.client.JSONNull");
    composerFactory.addImport("com.google.gwt.json.client.JSONNumber");
    composerFactory.addImport("com.google.gwt.json.client.JSONObject");
    composerFactory.addImport("com.google.gwt.json.client.JSONString");
    composerFactory.addImport("com.google.gwt.user.client.Window");
    // composerFactory.addImport("lovely.gwt.jsonrpc.client.JSONInteger");

    composerFactory.setSuperclass(JSONServiceBase.class.getName());
    composerFactory.addImplementedInterface(interfaceName);
    composerFactory.addImplementedInterface(ServiceDefTarget.class
        .getSimpleName());

    return composerFactory.createSourceWriter(context, printWriter);
  }

  private void print(String s) {
    wrapperSourceWriter.print(s);
    if (debugWriter != null) {
      debugWriter.append(s);
    }
  }

  private void println(String s) {
    wrapperSourceWriter.println(s);
    if (debugWriter != null) {
      debugWriter.append(s);
      debugWriter.append('\n');
      if (debugIndent > 0)
        debugWriter.append(tabString.substring(0, debugIndent));
    }
  }

  private void indent() {
    wrapperSourceWriter.indent();
    debugIndent++;
  }

  private void outdent() {
    wrapperSourceWriter.outdent();
    debugIndent--;
  }

  private static String simpleStubClassName(String className) {
    return "__" + className + "_impl__";
  }

  private class NameTypePair {
    public final String Name;
    public final JType Type;

    public NameTypePair(String name, JType type) {
      Name = name;
      Type = type;
    }
  }
}
